/*
 *    AutoClassDiscovery.java
 *    Copyright (C) 2007 University of Waikato, Hamilton, New Zealand
 *    @author Richard Kirkby (rkirkby@cs.waikato.ac.nz)
 *
 *    This program is free software; you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation; either version 2 of the License, or
 *    (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with this program; if not, write to the Free Software
 *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package moa.core;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

public class AutoClassDiscovery {

	protected static final Map<String, String[]> cachedClassNames = new HashMap<String, String[]>();

	public static String[] findClassNames(String packageNameToSearch) {
		String[] cached = cachedClassNames.get(packageNameToSearch);
		if (cached == null) {
			HashSet<String> classNames = new HashSet<String>();
			StringTokenizer pathTokens = new StringTokenizer(System
					.getProperty("java.class.path"), File.pathSeparator);
			String packageDirName = packageNameToSearch.replace('.',
					File.separatorChar);
			String packageJarName = packageNameToSearch.length() > 0 ? (packageNameToSearch
					.replace('.', '/') + "/")
					: "";
			while (pathTokens.hasMoreElements()) {
				String pathToSearch = pathTokens.nextElement().toString();
				if (pathToSearch.endsWith(".jar")) {
					try {
						JarFile jar = new JarFile(pathToSearch);
						Enumeration<JarEntry> jarEntries = jar.entries();
						while (jarEntries.hasMoreElements()) {
							String jarEntry = jarEntries.nextElement()
									.getName();
							if (jarEntry.startsWith(packageJarName)) {
								String relativeName = jarEntry
										.substring(packageJarName.length());
								if (relativeName.endsWith(".class")) {
									relativeName = relativeName.replace('/',
											'.');
									classNames.add(relativeName.substring(0,
											relativeName.length()
													- ".class".length()));
								}
							}
						}
					} catch (IOException ignored) {
						// ignore unreadable files
					}
				} else {
					File root = new File(pathToSearch + File.separatorChar
							+ packageDirName);
					String[] names = findClassesInDirectoryRecursive(root, "");
					for (String name : names) {
						classNames.add(name);
					}
				}
			}
			cached = classNames.toArray(new String[classNames.size()]);
			Arrays.sort(cached);
			cachedClassNames.put(packageNameToSearch, cached);
		}
		return cached;
	}

	protected static String[] findClassesInDirectoryRecursive(File root,
			String packagePath) {
		HashSet<String> classNames = new HashSet<String>();
		if (root.isDirectory()) {
			String[] list = root.list();
			for (String string : list) {
				if (string.endsWith(".class")) {
					classNames.add(packagePath
							+ string.substring(0, string.length()
									- ".class".length()));
				} else {
					File testDir = new File(root.getPath() + File.separatorChar
							+ string);
					if (testDir.isDirectory()) {
						String[] names = findClassesInDirectoryRecursive(
								testDir, packagePath + string + ".");
						for (String name : names) {
							classNames.add(name);
						}
					}
				}
			}
		}
		return classNames.toArray(new String[classNames.size()]);
	}

	public static Class[] findClassesOfType(String packageNameToSearch,
			Class<?> typeDesired) {
		ArrayList<Class<?>> classesFound = new ArrayList<Class<?>>();
		String[] classNames = findClassNames(packageNameToSearch);
		for (String className : classNames) {
			String fullName = packageNameToSearch.length() > 0 ? (packageNameToSearch
					+ "." + className)
					: className;
			if (isPublicConcreteClassOfType(fullName, typeDesired)) {
				try {
					classesFound.add(Class.forName(fullName));
				} catch (Exception ignored) {
					// ignore classes that we cannot instantiate
				}
			}
		}
		return classesFound.toArray(new Class[classesFound.size()]);
	}

	public static boolean isPublicConcreteClassOfType(String className,
			Class<?> typeDesired) {
		Class<?> testClass = null;
		try {
			testClass = Class.forName(className);
		} catch (Exception e) {
			return false;
		}
		int classModifiers = testClass.getModifiers();
		return (java.lang.reflect.Modifier.isPublic(classModifiers)
				&& !java.lang.reflect.Modifier.isAbstract(classModifiers)
				&& typeDesired.isAssignableFrom(testClass) && hasEmptyConstructor(testClass));
	}

	public static boolean hasEmptyConstructor(Class<?> type) {
		try {
			type.getConstructor();
			return true;
		} catch (Exception ignored) {
			return false;
		}
	}

}
